package com.dbms.transaction;

import com.dbms.catalog.Catalog;
import com.dbms.common.Schema;

import java.io.EOFException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 事务日志管理器（LogManager）
 * 核心职责：
 * 1. 负责事务日志记录（LogRecord）的持久化存储（写入日志文件）
 * 2. 支持日志强制刷盘，确保日志不丢失（满足事务ACID的耐久性）
 * 3. 提供日志读取与反序列化能力（为数据库崩溃恢复提供数据支持）
 * 4. 管理内存中的事务日志缓存，便于快速获取单个事务的日志记录
 */
public class LogManager {

    // 日志文件操作对象：使用RandomAccessFile支持随机读写，模式为"rwd"（读写+同步写入磁盘，确保日志持久化）
    private final RandomAccessFile logFile;

    // 内存中的事务日志缓存：key=事务ID，value=该事务的所有日志记录列表
    // 使用ConcurrentHashMap保证多线程并发访问安全（如多事务同时写日志）
    private final Map<Integer, List<LogRecord>> transactionLogs = new ConcurrentHashMap<>();

    /**
     * 构造函数：初始化日志管理器，创建/打开指定的日志文件
     * @param logFileName 日志文件路径（如"transaction.log"）
     * @throws IOException 日志文件创建/打开失败时抛出（如权限不足、路径不存在）
     */
    public LogManager(String logFileName) throws IOException {
        // 打开日志文件，模式"rwd"的核心作用：
        // - r: 支持读操作（用于恢复时读取日志）
        // - w: 支持写操作（用于记录事务日志）
        // - d: 每次写入都会强制同步到物理磁盘，避免日志因内存缓存丢失
        this.logFile = new RandomAccessFile(logFileName, "rwd");
    }

    /**
     * 追加日志记录到日志文件（线程安全）
     * 核心流程：序列化日志记录 → 写入文件 → 缓存到内存事务日志列表
     * @param record 待写入的事务日志记录（如BEGIN/COMMIT/INSERT/UPDATE/DELETE）
     * @throws RuntimeException 日志写入失败时抛出（如磁盘空间不足、文件损坏）
     */
    public synchronized void appendLogRecord(LogRecord record) {
        try {
            // 1. 将日志记录序列化为字节数组（包含记录长度、类型、事务ID、数据等）
            byte[] recordData = record.serialize();

            // 2. 定位到日志文件末尾，追加写入序列化后的字节数组
            logFile.seek(logFile.length());
            logFile.write(recordData);

            // 3. 将日志记录缓存到内存的transactionLogs中，便于后续快速查询（如事务回滚时获取日志）
            int txnId = record.getTransactionId();
            // 若事务ID不存在则创建新列表，存在则直接添加记录
            transactionLogs.computeIfAbsent(txnId, k -> new ArrayList<>()).add(record);

        } catch (IOException e) {
            // 日志写入失败会导致事务无法保证耐久性，因此抛出运行时异常终止操作
            throw new RuntimeException("Could not write to log file", e);
        }
    }

    /**
     * 强制刷新日志文件缓冲区到物理磁盘
     * 作用：确保已写入的日志记录被持久化到磁盘，避免因操作系统缓存导致日志丢失
     * 场景：事务提交（COMMIT）前调用，满足事务ACID的耐久性要求
     * @throws RuntimeException 日志刷盘失败时抛出（如磁盘IO异常）
     */
    public void flushLog() {
        try {
            // 调用文件描述符的sync()方法，强制将操作系统缓存中的数据写入物理磁盘
            logFile.getFD().sync();
        } catch (IOException e) {
            throw new RuntimeException("Could not flush log file", e);
        }
    }

    /**
     * 【核心新增】读取整个日志文件，将所有日志记录反序列化为LogRecord列表
     * 核心用途：数据库崩溃恢复时，通过读取所有日志分析事务状态（已提交/未提交），执行回滚或重做
     * @param catalog 元数据目录，用于获取表的Schema结构（反序列化Tuple时需要表结构信息）
     * @return 包含所有日志记录的列表（按写入顺序排列）
     * @throws RuntimeException 日志读取/反序列化失败时抛出（如文件损坏、Schema不匹配）
     */
    public List<LogRecord> readAllLogs(Catalog catalog) {
        List<LogRecord> allRecords = new ArrayList<>();
        try {
            // 1. 定位到日志文件开头，准备从头读取
            logFile.seek(0);

            // 2. 构建表名→Schema的映射表：反序列化Tuple时需要知道表的列结构
            Map<String, Schema> schemaMap = new HashMap<>();
            for (String tableName : catalog.getTableNames()) {
                schemaMap.put(tableName, catalog.getTable(tableName).schema);
            }

            // 3. 循环读取每个日志记录，直到文件末尾
            while (logFile.getFilePointer() < logFile.length()) {
                try {
                    // 3.1 读取日志记录的长度字段（前4个字节，存储记录总长度）
                    byte[] sizeBytes = new byte[4];
                    int bytesRead = logFile.read(sizeBytes);
                    if (bytesRead < 4) break; // 读取到不完整的长度字段，视为文件末尾

                    // 3.2 解析记录长度（字节数组转int）
                    int recordSize = ByteBuffer.wrap(sizeBytes).getInt();

                    // 3.3 读取完整的日志记录数据（包含长度字段本身）
                    byte[] recordData = new byte[recordSize];
                    // 先将长度字段写回字节数组（反序列化时需要完整的记录数据，包含长度）
                    ByteBuffer.wrap(recordData).putInt(recordSize);
                    // 读取剩余的记录数据（从第4个字节开始，共recordSize-4字节）
                    logFile.read(recordData, 4, recordSize - 4);

                    // 3.4 反序列化字节数组为LogRecord对象（需要Schema映射表解析Tuple）
                    LogRecord record = LogRecord.deserialize(recordData, schemaMap);
                    if (record != null) {
                        allRecords.add(record);
                    }
                } catch (EOFException e) {
                    // 正常到达文件末尾，退出循环（避免异常中断）
                    break;
                }
            }
        } catch (IOException e) {
            throw new RuntimeException("Could not read from log file", e);
        }
        return allRecords;
    }

    /**
     * 根据事务ID获取该事务的所有日志记录
     * 用途：事务回滚时，需要获取该事务的所有操作日志，执行undo操作
     * @param txnId 事务ID
     * @return 该事务的日志记录列表（若事务不存在，返回空列表）
     */
    public List<LogRecord> getLogsForTransaction(int txnId) {
        // 若事务ID未在缓存中，返回空列表（避免空指针异常）
        return transactionLogs.getOrDefault(txnId, new ArrayList<>());
    }

    /**
     * 清除指定事务的内存日志缓存
     * 用途：事务提交（COMMIT）或回滚（ROLLBACK）后，该事务的日志不再需要，清理内存释放资源
     * @param txnId 事务ID
     */
    public void clearLogsForTransaction(int txnId) {
        transactionLogs.remove(txnId);
    }

    /**
     * 关闭日志文件，释放资源
     * 场景：数据库关闭时调用，确保日志文件正常关闭，避免资源泄漏
     * @throws IOException 日志文件关闭失败时抛出（如文件被占用）
     */
    public void close() throws IOException {
        logFile.close();
    }
}